require 'rss/1.0'
require 'rss/2.0'

require 'yaml'
require "pstore"
require 'delegate'
require 'open-uri'
require 'acts_as_sentence'
require "#{OS}_buildin_actions" if defined?(OS)

class Object
  def system(*args)
    Thread.new do
      super(*args)
    end
  end
end

module CcMonitor

  VERSION = '1.2'

  def request(uri)
    logger.debug { "request: #{uri}" }
    open(uri) do |s|
      content = s.read
      logger.debug { "uri content: #{content}" }
      yield content
    end
  end

  def run(uri)
    CcMonitor::Runner.run uri
  end

  def update
    updater = CcMonitor::Updater.new
    updater.refresh
  end

  module_function :request, :run, :update

  class Build

    attr_accessor :succeeded, :name

    def initialize(name, rss_reader)
      @name = name.nameize
      @rss_reader = rss_reader
      @committer_names = {}
    end

    def refresh
      @succeeded = nil
      @rss_reader.refresh
    end

    def succeeded?
      return @succeeded if defined?(@succeeded) && !@succeeded.nil?

      begin
        rss_item_title =~ /(SUCCESS|FIXED|PASSED)/i
      rescue
        false
      end
    end

    def failed?
      !self.succeeded?
    end
    
    def loaded_error?
      did_not_get_rss_item?
    end

    def did_not_get_rss_item?
      @rss_reader.latest_item.nil?
    end

    def status
      return 'loaded error' if did_not_get_rss_item?
      self.succeeded? ? 'succeeded' : 'failed'
    end

    def message
      "#{@name} build #{self.status}."
    end

    def link
      rss_item_link || @rss_reader.rss_uri
    end

    def new?
      rss_item_new?
    end
    
    def committers(real_names={})
      return @committer_names.merge!(real_names) unless real_names.empty?

      if rss_item_description
        committers = looking_for_committers_in_the_rss_item_description
        committers = looking_for_committers_in_the_page_get_from_rss_link if committers.empty?
        return "" if committers.empty?

        last = committers.pop

        %{committed by #{committers.empty? ? last : committers.join(', ') + ' and ' + last}.}
      else
        "Don't know committers."
      end
    end
    
    def looking_for_committers_in_the_rss_item_description
      committers = []
      rss_item_description.gsub(/committed by (\w+)/) do
        committers << (@committer_names[$1] || $1)
      end
      committers.compact.uniq
    end
    
    def looking_for_committers_in_the_page_get_from_rss_link
      CcMonitor.request(rss_item_link) do |content|
        committers = []
        content.gsub(/<td title="user" class="user">([^<]+)<\/td>/) do
          committers << (@committer_names[$1] || $1)
        end
        committers.compact.uniq
      end
    end

    def method_missing(method, *args)
      if method.id2name =~ /^rss_item_(.*)/
        return (@rss_reader.latest_item ? @rss_reader.latest_item.send($1, *args) : nil)
      elsif method.id2name =~/_and_/
        methods = method.id2name.split('_and_')
        result = ''
        methods.each do |m|
          result << self.send(m, *args) << ' ' if self.respond_to?(m)
        end
        return result
      elsif method.id2name == (name.gsub(/\?+$/, '') + '?')
        return true
      end
      super
    end
  end

  class Runner
    
    def self.run(commands_uri)
      new(commands_uri).action
    end

    def initialize(commands_uri)
      @commands_uri = commands_uri
    end

    def action
      output = Output.new
      begin
        CcMonitor.request(@commands_uri) do |commands|
          # logger.debug { "going to execute commands: #{commands}" }
          CommandsExecutor.new(commands, output).execute
        end
      rescue Exception => e
        logger.error { e }
        output.log(e)
      end
      logger.debug { "output : #{output.inspect}" }
      output
    end

  end

  class CommandsExecutor
    @@repository = {}

    include BuildinActions
    include ActsAsSentence::Base
    
    def initialize(commands='', output=Output.new)
      @commands = commands
      @output = output
      @builds = {}
      @real_names = {}
      @build_working_on = nil
    end

    def execute
      begin
        eval(@commands)
      rescue Exception => e
        log_runtime_error e
      end
      @output
    end
    
    def log_runtime_error(e)
      logger.error { e }
      e.message.gsub!(/^\(eval\)/, '(your cc monitor)')
      @output.log(e)
    end

    def keep_eyes_on(*builds, &block)
      builds = builds[0] if builds.size == 1 && builds[0].kind_of?(Array)
      
      builds.each do |build|
        begin
          @build_working_on = build
          keep_eyes_on_a build, &block
          @build_working_on = nil
        rescue Exception => e
          log_runtime_error e
        end
      end
    end
    
    def keep_eyes_on_a(build)
      result = build.refresh

      if block_given? 
        logger.info { "yield keep_eyes_on's block on #{build.name}, new build?: #{build.new?}" }
        yield build
      else
        logger.info { "block not given for #{build.name}, display build status icon" }
        display build(status_icon)
      end

      change_panel_background_to :green
      change_panel_background_to :red if build.failed?
      change_panel_background_to :default if build.loaded_error?

    end

    def display(build_status)
      if build_status.respond_to?(:call_similarest)
        build_status.send :call_similarest, @output.export(@build_working_on)
      else
        build_status.each do |status|
          status.send :call_similarest, @output.export(@build_working_on) if status.respond_to?(:call_similarest)
        end
      end
    end

    def change_panel_background_to color
      @output.change_background_to color
    end

    def def_build(*args)
      return unless args.size == 2 || args[0].kind_of?(Hash)
      name, rss_uri = if args[0].kind_of?(Hash)
        key = args[0].keys[0]
        [key, args[0][key]]
      else
        args
      end
      build = CcMonitor::Build.new(name, create_rss_reader(rss_uri))
      build.committers @real_names
      @builds[build.name] = build
    end

    def create_rss_reader(rss_uri)
      CcMonitor::RssReader.new rss_uri
    end
    
    def committers(*args)
      if args.size == 1 && args[0].kind_of?(Hash)
        real_names = args[0]
      else
        return method_missing(:committers, *args)
      end
      real_names.each_pair do |committer, real_name|
        real_names[committer.to_s] = real_name.to_s
      end
      
      @real_names.merge! real_names
      @builds.each_value do |build|
        build.committers real_names
      end
    end
        
    def object_exists(identifier)
      @builds[identifier.nameize]
    end
    
    alias_method :kernel_load, :load
    undef :load
    
    def cant_load?(build)
      build.loaded_error?
    end
    
    def can_load?(build)
      !build.loaded_error?
    end
    
    def build(*opt)
      return @build_working_on if opt.empty?

      if opt[0].kind_of?(Hash)
        def_build(*opt)
      else
        result = opt.collect do |sentence|
          sentence.call_similarest(@build_working_on)
        end
        if result.size == 1 then
          return result.first
        else
          return *result
        end
      end
    end
    
    def sentence_methods
      {
        :keep_eyes_on.nameize => :keep_eyes_on, 
        :observe.nameize => :keep_eyes_on, 
        :def_build.nameize => :def_build,
        :cant_load.nameize => :cant_load?,
        :can_load.nameize => :can_load?
      }
    end
  end

  class Output < Hash
    COLOR_ORDER = {:default => 3, :red => 2, :green => 1}
    
    def export(build)
      BuildOutput.new(self, build)
    end
    
    def log(error)
      array_of(:errors) << error
    end
    
    def change_background_to(color)
      self[:background] = color if COLOR_ORDER[color] > (COLOR_ORDER[self[:background]] || 0)
    end
    
    def add_link(link)
      array_of(:links) << link
    end
    
    def add_icon(icon)
      array_of(:icons) << icon
    end
    
    private
    def array_of(key)
      (self[key] ||= [])
    end
  end

  class BuildOutput
    def initialize(output, build)
      @build = build
      @output = output
    end

    def message_link
      @output.add_link :url => @build.link, :message => @build.message, :description => @build.committers
    end

    def status_icon
      @output.add_icon :status => @build.status, :link => @build.link, :description => "#{@build.message} #{@build.committers} link: #{@build.link}"
    end
  end
  class RssReader

    attr_accessor :rss_uri
    attr_reader :latest_item

    def initialize(rss_uri)
      @rss_uri = rss_uri
      @repository = Repository.new
    end

    def refresh
      @latest_item = load_latest_item
    end

    def load_latest_item
      request_rss_item do |rss|
        store(rss.items[0])
      end
    end

    private

    def request_rss_item
      begin
        rss = CcMonitor.request(@rss_uri) do |content|
          RSS::Parser.parse content, false
        end
      rescue Exception => e
        nil
      else
        yield rss
      end
    end

    def store(item)
      stored_latest_item = @repository[@rss_uri]
      is_new = stored_latest_item ? item.title != stored_latest_item.title : true
      
      if is_new
        logger.info { "new rss from: #{@rss_uri}, title: #{item.title}; stored_latest_item title: #{stored_latest_item ? stored_latest_item.title : 'no stored item'} " }
        @repository[@rss_uri] = item
      end
      RssItem.new item, is_new
    end
  end

  class RssItem < DelegateClass(RSS::Rss::Channel::Item)
    def initialize(item, is_new)
      super(item)
      @is_new = is_new
    end

    def new?
      @is_new
    end
  end

  class Repository

    FILE_NAME = 'ccm.repository.pstore'

    def self.destroy
      File.delete(FILE_NAME) if File.exist?(FILE_NAME)
    end

    def [](key)
      return nil unless File.exist?(FILE_NAME)
      
      repository = PStore.new(FILE_NAME)
      repository.transaction(true) do
        repository[key]
      end
    end

    def []=(key, value)
      repository = PStore.new(FILE_NAME)
      repository.transaction do
        repository[key] = value
      end
    end
  end

  class Updater

    DOWNLOAD_HOME = 'http://ccmonitor.googlecode.com/files/'

    attr_reader :latest_version, :latest_version_description, :latest_version_link

    def initialize(uri=nil)
      @uri = uri.nil? ? self.source : uri
    end

    def source
      # cc-monitor-X.X-update.yaml
      return DOWNLOAD_HOME + 'cc-monitor-' + CcMonitor::VERSION + '-update.yaml';
    end

    def refresh
      begin
        result = CcMonitor.request(@uri) do |content|
          YAML.load(content)
        end
        @latest_version = result[:version]
        @latest_version_description = result[:description]
        @latest_version_link = result[:link]
        self
      rescue
      end
    end
  end
  
  class Service
    RUNNING = 'ccms.pid'
    STOP = 'ccms.stop.cmd'
    SETTED_URI = 'ccms.set_uri.cmd'
    SETTED_INTERVAL = 'ccms.set_interval.cmd'
    OUTPUT = 'ccms.output'
    
    def start
      cmd(STOP)
      if running?
        info 'ccm service is already running.'
      else
        run
      end
    end
    
    def stop
      create_with_time_stamp(STOP)
      info 'stopping ccm service'
    end
    
    def set_uri(uri)
      create(SETTED_URI, uri)
      info "setted ccm file uri: #{uri}"
    end
    
    def set_interval(interval=60)
      create(SETTED_INTERVAL, interval)
      info "setted interval: #{interval}"
    end
    
    def output
      if File.exist?(OUTPUT)
        File.open(OUTPUT) do |file|
          YAML::load(file) || {}
        end
      else
        {}
      end
    end
    
    def running?
      if File.exist?(RUNNING)
        locked = running_status
        info "flag(#{RUNNING}) exists and #{locked ? 'can been' : 'can not been'} locked."
        cmd(RUNNING) if locked
        !locked
      end
    end
    
    private

    def running_status
      file = File.new(RUNNING, 'w+')
      
      begin
        locked = file.flock(File::LOCK_EX | File::LOCK_NB)
        yield locked if block_given?
        return locked
      ensure
        file.close
      end
    end
    
    def run
      create_with_time_stamp(RUNNING)
      begin
        running_status do |locked|
          return unless locked
          info 'ccm service started.'
          until stop?
            refresh_uri
            info "working on ccm: #{@uri}"
            store CcMonitor.run(@uri) if @uri
            info 'sleeping...'
            sleep(interval)
          end
        end
      rescue Exception => e
        logger.error { 'ccms crashed by error. ' }
        logger.error { e }
      ensure
        cmd(RUNNING)
        info 'ccm service stopped.'
      end
    end

    def create_with_time_stamp(file)
      create(file, Time.now.to_s)
    end    

    def info(message)
      logger.info { message }
      puts message
    end
    
    def store(output)
      create OUTPUT, output.to_yaml
    end
    
    def stop?
      cmd(STOP) || !File.exist?(RUNNING) || running_dir_changed?
    end

    def running_dir_changed?
      File.expand_path(__FILE__) != (@current_file ||= File.expand_path(__FILE__))
    end
    
    def refresh_uri
      @uri = get(SETTED_URI) || @uri
    end
    
    def interval
      @interval = (get(SETTED_INTERVAL) || @interval || 20).to_i
      @interval = 15 if @interval < 15
      @interval
    end
    
    def cmd(file)
      if File.exist?(file)
        r = File.delete(file)
        logger.debug { "deleted #{file}: #{r}" }
      end
    end
    
    def get(file)
      @result = nil
      if File.exist?(file)
        File.open(file) do |io|
          @result = (io.read || '').strip
        end
      end
      @result
    end
    
    def create(file, content)
      result = File.open(file, 'w') do |file|
        file.syswrite(content)
      end
      logger.debug { "create #{file}: #{result}" }
    end
  end
end
